﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections.Generic;
using System.ComponentModel;
using ScrumTable.BL.DM.DataManagement.Tracking;
using ScrumTable.BL.DM.Properties;
using ScrumTable.Common;
using ScrumTable.Common.Collections;
using ScrumTable.Common.EventHandling;
using ScrumTable.Common.Logging;
using ScrumTable.Common.Security;
using ScrumTable.Common.Threading;
using ScrumTable.DL.Data.Common;
using ScrumTable.DL.Data.Schema.ScrumTableV1;

#endregion

namespace ScrumTable.BL.DM.DataManagement
{
    /// <summary>
    /// Contains all data used by the domain layer, this is the entry
    /// point for the domain layer and contains all required business
    /// objects in order to manage data from a data source.
    /// </summary>
    internal class DomainContext : DisposeBase, IDomainContext
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private const string EvtCollectionChanged = "OnCollectionChanged";
        private const string EvtElementChanging = "OnElementChanging";
        private const string EvtElementChanged = "OnElementChanged";
        private const string EvtSyncError = "OnSyncError";

        private readonly DelegateHandlerCollection<DomainSyncErrorHandler> _syncErrorEvents = new DelegateHandlerCollection<DomainSyncErrorHandler>();
        private readonly DelegateHandlerCollection<DomainCollectionChangedEventHandler> _changedEvents = new DelegateHandlerCollection<DomainCollectionChangedEventHandler>();
        private readonly DelegateHandlerCollection<PropertyChangingEventHandler> _pptyChangingEvents = new DelegateHandlerCollection<PropertyChangingEventHandler>();
        private readonly DelegateHandlerCollection<PropertyChangedEventHandler> _pptyChangedEvents = new DelegateHandlerCollection<PropertyChangedEventHandler>();

        private readonly UndoManager _undoManager;
        private DomainCollection<Project> _projects;
        private IDataContext _data;
        private bool _isInitialized = false;

        private MultipleDomainCollectionChangedEventArgs _changedEventCollector;
        private MultipleDomainChangingEventArgs _pptyChangingEventCollector;
        private MultipleDomainChangedEventArgs _pptyChangedEventCollector;
        private IDisposable _updateHandler;

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        /// <summary>
        /// Returns true if the external sysetem is connected and can be used.
        /// </summary>
        public bool IsConnected
        {
            get
            {
                EnsureNotDisposed();
                return Data.Connection.IsConnected;
            }
        }

        /// <summary>
        /// Gets a list of all project.
        /// </summary>
        public DomainCollection<Project> Projects
        {
            get
            {
                if (_projects == null)
                {
                    _projects = new DomainCollection<Project>(
                        false,
                        this,
                        null,
                        Data.Storage[Schema.ProjectSchema]);
                }
                return _projects;
            }
        }

        /// <summary>
        /// Gets the current connection state.
        /// </summary>
        public ConnectionState ConnectionState
        {
            get
            {
                EnsureNotDisposed();
                return Data.Connection.ConnectionState;
            }
        }

        /// <summary>
        /// Gets the connection information which are provided by the user
        /// and used to establish a connection to the external data services.
        /// </summary>
        public ConnectionInformation ConnectionInformation
        {
            get
            {
                EnsureNotDisposed();
                return Data.Connection.ConnectionManager.Selected;
            }
        }

        /// <summary>
        /// Gets the underlying undo manager which is needed to track
        /// the state of the business layer entities.
        /// </summary>
        public UndoManager UndoManager { get { return _undoManager; } }

        /// <summary>
        /// Gets additional driver information from the underlying data layer.
        /// </summary>
        public DomainDriverInformation DriverInfo { get; private set; }

        /// <summary>
        /// Gets the data context which contains the data access objects.
        /// </summary>
        internal IDataContext Data
        {
            get
            {
                EnsureNotDisposed();
                return _data;
            }
        }

        /// <summary>
        /// Gets the associated data layer schema.
        /// </summary>
        internal ScrumTableV1Schema Schema
        {
            get { return Data.Schema; }
        }

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new DomainContext instance. This constructor
        /// is internal because its needed by the corresponding factory only.
        /// </summary>
        /// <param name="data">Data context which is given by the factory.</param>
        internal DomainContext(IDataContext data)
        {
            PreCondition.AssertNotNull(data, "data");
            
            _undoManager = new UndoManager(this);
            _data = data;
            _data.BeginUpdate += OnDataBulkUpdateBegin;
            _data.EndUpdate += OnDataBulkUpdateEnd;

            DriverInfo = new DomainDriverInformation(_data.Schema);
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Connects to the server instance. Make sure you disconnect from the
        /// server after you finished your work with the data connection.
        /// </summary>
        /// <param name="dispatcher">Specifies the sychronization dispatcher for the async callback.</param>
        /// <param name="credentials">Specifies the connection context which provides information about the end system and the user.</param>
        /// <param name="callback">Specifies the callback used to retrieve the connection state.</param>
        /// <param name="dataMoniker">Specifies a hint (moniker) to the data location. This must be supported by the underlying driver.</param>
        public void Connect(IDataSyncDispatcher dispatcher, ConnectionInformationManager credentials, AsyncCallback callback, string dataMoniker)
        {
            Logger<DomainContext>.InfoFormat(Resources.LogDmnCtxConnect, credentials);

            PreCondition.AssertNotNull(dispatcher, "dispatcher");
            PreCondition.AssertNotNull(credentials, "credentials");
            PreCondition.AssertNotNull(callback, "callback");

            EnsureNotDisposed();
            Data.DataSyncDispatcher = dispatcher;
            Data.Connection.Connect(credentials, callback, dataMoniker);
        }

        /// <summary>
        /// Changes the user and stores its changes to the back end system.
        /// </summary>
        /// <param name="credentials">Specifies the login credential information.</param>
        /// <param name="callbackEvent">Specifies the callback which is fired after the
        /// process has been completed. The callback is of type ICancellableDataResult
        /// and its Data property contains true, if all changes have been accepted.</param>
        public void ChangeUser(Member credentials, AsyncCallback callbackEvent)
        {
            EnsureNotDisposed();
            Data.Connection.ChangeUser(credentials.DataObject, callbackEvent);
        }

        /// <summary>
        /// Changes the user after the latests changes have been stored to the back end system.
        /// </summary>
        /// <param name="credentials">Specifies the login credential information.</param>
        public void ChangeUser(Member credentials)
        {
            EnsureNotDisposed();
            Data.Connection.ChangeUser(credentials.DataObject);
        }

        /// <summary>
        /// Disconnects from a connected server instance.
        /// </summary>
        public void Disconnect()
        {
            EnsureNotDisposed();
            DisposeChildren();
            Data.Connection.Disconnect();
        }

        /// <summary>
        /// Searches in the domain context tree for a domain object wiht the
        /// given id/type chain and returns the found element.
        /// </summary>
        /// <param name="typedIdChain">Specifies the id chain of the element to retrieve.</param>
        /// <returns>Returns the retrieved element.</returns>
        public DomainBase TryGetObjectByChain(ValueChain<Pair<string, Type>> typedIdChain)
        {
            EnsureNotDisposed();
            Pair<string, Type> projectChain = typedIdChain.Bottom;
            Project project = Projects.TryGetById(projectChain.Left);

            if (project != null)
            {
                ValueChain<Pair<string, Type>> subChain = typedIdChain.SubChain(1);

                if (!subChain.IsEmpty)
                {
                    return project.TryGetByChain(subChain);
                }
                return project;
            }
            return null;
        }

        /// <summary>
        /// Searches in the domain context tree for a domain object wiht the
        /// given id/type chain and returns the found element.
        /// </summary>
        /// <param name="typedIdChain">Specifies the id chain of the element to retrieve.</param>
        /// <returns>Returns the retrieved element.</returns>
        public T TryGetObjectByChain<T>(ValueChain<Pair<string, Type>> typedIdChain)
            where T : DomainBase, new()
        {
            DomainBase domainBase = TryGetObjectByChain(typedIdChain);

            if (domainBase is T)
            {
                return (T) domainBase;
            }
            return null;
        }

        /// <summary>
        /// Searches in the domain context tree for a domain object with the
        /// given id/type chain and returns the found element.
        /// </summary>
        /// <param name="typedIdChain">Specifies the id chain of the element to retrieve.</param>
        /// <typeparam name="T">Specifies the item type of the collection to retrieve.</typeparam>
        /// <returns>Returns the retrieved element.</returns>
        public DomainCollection<T> TryGetCollectionByChain<T>(ValueChain<Pair<string, Type>> typedIdChain)
            where T : DomainBase, new()
        {
            DomainBase parentElement = TryGetObjectByChain(typedIdChain);

            if (parentElement != null)
            {
                foreach (IDomainCollection collection in parentElement.ChildCollections)
                {
                    if (collection.ItemType == typeof (T))
                    {
                        return (DomainCollection<T>) collection;
                    }
                }
            }
            return null;
        }

        /// <summary>
        /// Searches in the domain context tree for a domain object with the
        /// given id/type chain and returns the found element. This procedure is slower than
        /// the ValueChain&gt;Pair&gt;string, Type&lt;&lt; variant and not type safe.
        /// </summary>
        /// <param name="typedIdChain">Specifies the id chain of the element to retrieve.</param>
        /// <typeparam name="T">Specifies the item type of the collection to retrieve.</typeparam>
        /// <returns>Returns the retrieved element.</returns>
        public DomainCollection<T> TryGetCollectionByChain<T>(ValueChain<string> typedIdChain)
            where T : DomainBase, new()
        {
            DomainBase parentElement = TryGetObjectByChain(typedIdChain);

            if (parentElement != null)
            {
                foreach (IDomainCollection collection in parentElement.ChildCollections)
                {
                    if (collection.ItemType == typeof(T))
                    {
                        return (DomainCollection<T>)collection;
                    }
                }
            }
            return null;
        }

        /// <summary>
        /// Disables the CollectionChanged event on domain context scope level.
        /// </summary>
        /// <returns>
        /// Returns a disposable object which is used to fire the result event.
        /// The Action property of this event is set to DomainCollectionChangedAction.None and
        /// the Owner and ChangedItem properties are set to null.
        /// </returns>
        public IDisposable DisableCollectionChangedEvents()
        {
            _changedEventCollector = new MultipleDomainCollectionChangedEventArgs();
            return new DisposeAction(DisableCollectionChangedEventsEnd);
        }

        /// <summary>
        /// Disables the ElementChanging event on domain context scope level.
        /// </summary>
        /// <returns>
        /// Returns a disposable object which is used to fire the result event.
        /// The ChangedItem property is set to null and the ChangedElement contains an
        /// empty string.
        /// </returns>
        public IDisposable DisableElementChangingEvents()
        {
            _pptyChangingEventCollector = new MultipleDomainChangingEventArgs();
            return new DisposeAction(DisableElementChangingEventsEnd);
        }

        /// <summary>
        /// Disables the ElementChanged event on domain context scope level.
        /// </summary>
        /// <returns>
        /// Returns a disposable object which is used to fire the result event.
        /// The ChangedItem property is set to null and the ChangedElement contains an
        /// empty string.
        /// </returns>
        public IDisposable DisableElementChangedEvents()
        {
            _pptyChangedEventCollector = new MultipleDomainChangedEventArgs();
            return new DisposeAction(DisableElementChangedEventsEnd);
        }

        /// <summary>
        /// Releases managed (c# or .net) resources.
        /// </summary>
        protected override void ReleaseManagedResources()
        {
            DisposeChildren();

            if (_data != null)
            {
                _data.BeginUpdate -= OnDataBulkUpdateBegin;
                _data.EndUpdate -= OnDataBulkUpdateEnd;
                _data.Dispose();
            }

            _changedEvents.Dispose();
            _pptyChangingEvents.Dispose();
            _pptyChangedEvents.Dispose();
            _data = null;
        }

        private DomainBase TryGetObjectByChain(ValueChain<string> typedIdChain)
        {
            EnsureNotDisposed();
            string projectChain = typedIdChain.Bottom;
            Project project = Projects.TryGetById(projectChain);

            if (project != null)
            {
                ValueChain<string> subChain = typedIdChain.SubChain(1);

                if (!subChain.IsEmpty)
                {
                    return project.TryGetByChain(subChain);
                }
                return  project;
            }
            return null;
        }

        private void DisposeChildren()
        {
            if (_projects == null || !_isInitialized)
                return;

            _projects.ForEach(project => project.Dispose());
            _projects = null;
            _isInitialized = false;
        }


        private void DisableCollectionChangedEventsEnd()
        {
            DomainCollectionChangedEventArgs args = _changedEventCollector;
            bool fireUpdate = (_changedEventCollector != null && _changedEventCollector.EventCount > 0);
            _changedEventCollector = null;

            if (fireUpdate)
            {
                FireCollectionChanged(args);
            }
        }

        private void DisableElementChangingEventsEnd()
        {
            DomainChangingEventArgs args = _pptyChangingEventCollector;
            bool fireUpdate = (_pptyChangingEventCollector != null && _pptyChangingEventCollector.EventCount > 0);
            _pptyChangingEventCollector = null;

            if (fireUpdate)
            {
                FireElementChanging(args);
            }
        }

        private void DisableElementChangedEventsEnd()
        {
            DomainChangedEventArgs args = _pptyChangedEventCollector;
            bool fireUpdate = (_pptyChangedEventCollector != null && _pptyChangedEventCollector.EventCount > 0);
            _pptyChangedEventCollector = null;

            if (fireUpdate)
            {
                FireElementChanged(args);
            }
        }

        private void BulkBeginUpdate()
        {
            BulkEndUpdate();
            _updateHandler = new DataBulkUpdateHandler(this);
        }

        private void BulkEndUpdate()
        {
            if (_updateHandler != null)
            {
                _updateHandler.Dispose();
                _updateHandler = null;
            }
        }

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        /// <summary>
        /// This event is fired if an synchronization error has occurred on
        /// the underlying data storage.
        /// </summary>
        public event DomainSyncErrorHandler SyncError
        {
            add { _syncErrorEvents.Add(EvtSyncError, value); }
            remove { _syncErrorEvents.Remove(EvtSyncError, value); }
        }

        /// <summary>
        /// This event is fired if an element has been added or removed from
        /// a collection.
        /// </summary>
        public event DomainCollectionChangedEventHandler CollectionChanged
        {
            add { _changedEvents.Add(EvtCollectionChanged, value); }
            remove { _changedEvents.Remove(EvtCollectionChanged, value); }
        }

        /// <summary>
        /// This event is fired before an element has been 
        /// changed from the collection.
        /// </summary>
        public event PropertyChangingEventHandler ElementChanging
        {
            add { _pptyChangingEvents.Add(EvtElementChanging, value); }
            remove { _pptyChangingEvents.Remove(EvtElementChanging, value); }
        }

        /// <summary>
        /// This event is after before an element has been 
        /// changed from the collection.
        /// </summary>
        public event PropertyChangedEventHandler ElementChanged
        {
            add { _pptyChangedEvents.Add(EvtElementChanged, value); }
            remove { _pptyChangedEvents.Remove(EvtElementChanged, value); }
        }

        /// <summary>
        /// Fires the CollectionChanged event.
        /// </summary>
        /// <param name="args">Arguments to pass to the event receiver.</param>
        internal void FireCollectionChanged(DomainCollectionChangedEventArgs args)
        {
            if (_changedEventCollector != null)
                _changedEventCollector.Add(args);
            else
                _changedEvents.Fire(EvtCollectionChanged, this, args);
        }

        /// <summary>
        /// Fires the Changing event.
        /// </summary>
        /// <param name="args">Arguments to pass to the event receiver.</param>
        internal void FireElementChanging(DomainChangingEventArgs args)
        {
            if (_pptyChangingEventCollector != null)
                _pptyChangingEventCollector.Add(args);
            else
                _pptyChangingEvents.Fire(EvtElementChanging, this, args);
        }

        /// <summary>
        /// Fires the Changed event.
        /// </summary>
        /// <param name="args">Arguments to pass to the event receiver.</param>
        internal void FireElementChanged(DomainChangedEventArgs args)
        {
            if (_pptyChangedEventCollector != null)
                _pptyChangedEventCollector.Add(args);
            else
                _pptyChangedEvents.Fire(EvtElementChanged, this, args);
        }

        private void FireErrorEvents(IList<IDataSyncError> errors)
        {
            foreach (IDataSyncError error in errors)
            {
                DomainBase domainObject = TryGetObjectByChain(error.IdChain);

                if (domainObject != null)
                {
                    domainObject.NotifySynchronizationErrors(error.ErrorInfo);

                    _syncErrorEvents.Fire(
                        EvtSyncError,
                        this,
                        new DomainSyncErrorArgs(domainObject));
                }
            }
        }

        private void OnDataBulkUpdateEnd(object sender, UpdateEndEventArgs e)
        {
            BulkEndUpdate();
            FireErrorEvents(e.Errors);
        }

        private void OnDataBulkUpdateBegin(object sender, EventArgs e)
        {
            BulkBeginUpdate();
        }

        #endregion


        //--------------------------------------------------------------------
        // Nested Classes
        //--------------------------------------------------------------------

        private class DataBulkUpdateHandler : DisposeBase
        {
            #region Declarations
            //--------------------------------------------------------------------
            // Declarations
            //--------------------------------------------------------------------

            private readonly DomainContext _outer;
            private readonly IDisposable _collectionChangedEvent;
            private readonly IDisposable _elementChangedEvent;
            private readonly IDisposable _elementChangingEvent;

            #endregion

            #region Constructors / Destructor
            //--------------------------------------------------------------------
            // Constructors / Destructor
            //--------------------------------------------------------------------

            internal DataBulkUpdateHandler(DomainContext outer)
            {
                PreCondition.AssertNotNull(outer, "outer");

                _outer = outer;
                _collectionChangedEvent = _outer.DisableCollectionChangedEvents();
                _elementChangedEvent = _outer.DisableElementChangedEvents();
                _elementChangingEvent = _outer.DisableElementChangingEvents();
            }

            #endregion

            #region Methods
            //--------------------------------------------------------------------
            // Methods
            //--------------------------------------------------------------------

            /// <summary>
            /// Releases managed (c# or .net) resources.
            /// </summary>
            protected override void ReleaseManagedResources()
            {
                _elementChangingEvent.Dispose();
                _elementChangedEvent.Dispose();
                _collectionChangedEvent.Dispose();
            }

            #endregion
        }
    }
}